﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using IronLua.Metadata;

namespace IronLua.Runtime
{
    internal static class VirtualMachine
    {
        public const int MultipleResults = -1;
        public const int MaxTagLoop = 100;

        private static bool PreCall(Thread thread, Value callable, int callableStackIndex, int actualParamCount, int expectedResultCount)
        {
            var stack = thread.Stack;
            var currentBaseStackIndex = thread.CurrentFrame.BaseStackIndex;
            Func<int, int> baseOffset = (int i) => currentBaseStackIndex + i;
            if (callable.IsFunction)
            {
                var fn = callable.Function;
                var proto = fn.Prototype;
                var frame = new CallFrame(
                    proto: proto,
                    programCounter: 0,
                    baseStackIndex: baseOffset(callableStackIndex + 1),
                    returnStackIndex: baseOffset(callableStackIndex), 
                    expectedResultCount: expectedResultCount);

                switch (proto.Kind)
                {
                    case PrototypeKind.IL:
                        {                            
                            thread.Frames.Push(frame);
                            var clrfn = proto.ClrPrototype;
                            var actualResultCount = clrfn(thread);
                            VirtualMachine.ReturnFromCall(thread, currentBaseStackIndex, actualResultCount);
                            return true;
                        }
                    case PrototypeKind.Bytecode:
                        {
                            var expectedParamCount = (int)proto.ParameterCount;
                            var maxStack = (int)proto.StackSize;

                            var missingParamCount = expectedParamCount - actualParamCount;
                            for (int i = 0; i < missingParamCount; i++)
                            {
                                stack[baseOffset(i)] = Value.Nil;
                            }

                            if ((int)proto.VarArgFlags > 0)
                            {
                                for (int i = 0; i < expectedParamCount; i++)
                                {
                                    stack[baseOffset(actualParamCount + i)] = stack[baseOffset(i)];
                                    stack[baseOffset(i)] = Value.Nil;
                                }
                                thread.CurrentFrame.BaseStackIndex = baseOffset(actualParamCount);
                            }

                            //var stackSlotsToFill = maxStack - expectedParamCount;
                            //for (int i = 0; i < stackSlotsToFill; i++)
                            //{
                            //    stack[baseOffset(i)] = Value.Nil;
                            //}

                            thread.Frames.Push(frame);
                            return false;
                        }
                    default:
                        throw new LuaRuntimeException("unexpected prototype kind.");
                }
            }

            throw new NotImplementedException();
        }

        public static void Call(Thread thread, Value callable, int callableStackIndex, int actualParamCount, int expectedResultCount)
        {
            if (callable.IsNil)
            {
                throw new LuaRuntimeException("callable expected, got nil.");
            }

            if (VirtualMachine.PreCall(thread, callable, callableStackIndex, actualParamCount, expectedResultCount))
            {
                // clr method call
                return;
            }

            var actualResultCount = VirtualMachine.ExecuteBytecode(thread, callable);
        }

        private static void ReturnFromCall(Thread thread, int returnStackIndex, int actualResultCount)
        {
            var stack = thread.Stack;
            CallFrame frame;
            if (!thread.Frames.TryProp(out frame))
            {
                throw new LuaRuntimeException("too many returns.");
            }
            var frameReturnStackIndex = frame.ReturnStackIndex;
            var baseIndex = frame.BaseStackIndex;
            var resultRegisterOffset = frameReturnStackIndex - baseIndex;
            var resultCount = frame.WantedResults == VirtualMachine.MultipleResults
                                  ? actualResultCount
                                  : frame.WantedResults;

            var results = stack.Skip(baseIndex).Take(resultCount).ToArray();
            var pushedResultCount = resultCount > results.Length ? results.Length : resultCount;

            for (int j = 0; j < pushedResultCount; j++)
            {
                stack[baseIndex + resultRegisterOffset + j] = results[j];
            }

            stack.TryPopFrom(baseIndex + resultRegisterOffset + pushedResultCount);
        }

        private static int ExecuteBytecode(Thread thread, Value callable)
        {
        newframe:
            var stack = thread.Stack;
            var frame = thread.Frames.Current;
            var baseIndex = frame.BaseStackIndex;
            if (!callable.IsFunction)
            {
                throw new LuaRuntimeException("function expected.");
            }
            var cl = callable.Function;
            var proto = cl.Prototype;
            var consts = proto.Constants;
            var protos = proto.Prototypes;
            while (true)
            {
                var pc = frame.ProgramCounter++;

                if (pc >= proto.Instructions.Count)
                {
                    throw new LuaRuntimeException("program counter out of range.");
                }

                var i = proto.Instructions[pc];
                Func<Instruction, int> getRa = (instr) => baseIndex + (int)instr.A;
                Func<Instruction, int> getRb = (instr) => baseIndex + (int)instr.B;
                Func<Instruction, int> getRc = (instr) => baseIndex + (int)instr.C;
                Func<Instruction, Value> k = (instr) => consts[(int)instr.Bx].Value;
                Func<Instruction, Value> getRkb = (instr) => instr.IsBK ? consts[(int)instr.BKIndex].Value : stack[getRb(instr)];
                Func<Instruction, Value> getRkc = (instr) => instr.IsCK ? consts[(int)instr.CKIndex].Value : stack[getRc(instr)];

                var ra = getRa(i);

                switch (i.OpCode)
                {
                    case OpCodes.Move:
                        {
                            stack[ra] = stack[getRb(i)];
                        }
                        break;

                    case OpCodes.LoadK:
                        {
                            var ki = k(i);
                            stack[ra] = ki;
                        }
                        break;
                    case OpCodes.GetTabUp:
                        {
                            var b = (int)i.B;
                            var upvalue = cl.UpValues[b];
                            stack[ra] = VirtualMachine.TableGet(thread, ref upvalue, getRkc(i));
                        }
                        break;
                    case OpCodes.SetTabUp:
                        {
                            var a = (int)i.A;
                            var upvalue = cl.UpValues[a];
                            VirtualMachine.TableSet(thread, ref upvalue, getRkb(i), getRkc(i));
                        }
                        break;
                    case OpCodes.GetUpVal:
                        {
                            var b = (int)i.B;
                            stack[ra] = cl.UpValues[b];
                        }
                        break;
                    case OpCodes.SetUpVal:
                        {
                            var b = (int)i.B;
                            cl.UpValues[b] = stack[ra];
                        }
                        break;
                    case OpCodes.Mul:
                        {
                            var rkb = getRkb(i);
                            var rkc = getRkc(i);
                            stack[ra] = VirtualMachine.Arith(thread, rkb, rkc, (a, b) => a * b, MetaMethods.Mul);
                        }
                        break;
                    case OpCodes.TailCall:
                    case OpCodes.Call:
                        {
                            // HACK: tailcalls not properly implemented yet
                            var callee = stack[ra];
                            var calleeIndex = ra;
                            var paramCount = i.B == 0 
                                ? stack.Count - ra + 1
                                : (int)i.B - 1;
                            var expectedResultCount = (int)i.C - 1;
                            VirtualMachine.Call(thread, callee, calleeIndex, paramCount, expectedResultCount);
                        }
                        break;
                    case OpCodes.Return:
                        {
                            var b = (int)i.B;
                            var actualResultCount = b == 0
                                ? stack.Count - ra
                                : b - 1;

                            VirtualMachine.ReturnFromCall(thread, ra, actualResultCount);

                            return actualResultCount;
                        }
                    case OpCodes.Closure:
                        var clproto = protos[(int)i.Bx];
                        stack[ra] = VirtualMachine.CreateClosure(thread, frame.BaseStackIndex, clproto, cl.UpValues);
                        break;

                    default:
                        throw new NotImplementedException(i.OpCode.ToString());
                }

            }

            throw new NotImplementedException();
        }

        private static Value CreateClosure(Thread thread, int callerStackIndex, Prototype proto, IList<Value> enclosingUpValues)
        {
            var stack = thread.Stack;
            return Value.FromFunction(new Closure(proto, proto.UpValueMetadata.Select(upvmd =>
                upvmd.InStack
                ? stack[callerStackIndex + (int)upvmd.Index]
                : enclosingUpValues[(int)upvmd.Index])));
        }

        private static Value TableGet(Thread thread, ref Value t, Value key)
        {
            const int maxloop = VirtualMachine.MaxTagLoop;
            var loop = 0;
            var tm = t;
            var indexTag = MetaMethods.Index.ToMetaMethodString();
            while (loop++ < maxloop)
            {
                if (tm.IsTable)
                {
                    var table = tm.Table;
                    var oldValue = table[key];
                    var noKey = oldValue.IsDeadKey || oldValue.IsNil;
                    tm = VirtualMachine.GetMetaMethod(thread, table, indexTag);
                    var noMetaMethod = tm.IsNil;

                    if (noKey || noMetaMethod)
                    {
                        return table[key];
                    }
                }
                else
                {
                    tm = VirtualMachine.GetMetaMethod(thread, tm, indexTag);
                }

                if (tm.IsFunction)
                {
                    Value value;
                    VirtualMachine.CallMetaMethod(thread, tm, t, key, Value.Nil, result: out value);
                    return value;
                }
                else if (tm.IsNil)
                {
                    throw new LuaRuntimeException("attempt to index a {0} value.".FormatWith(t.GetTypeString()));
                }
            }

            throw new LuaRuntimeException("loop in gettable");
        }

        private static void TableSet(Thread thread, ref Value t, Value key, Value value)
        {
            const int maxloop = VirtualMachine.MaxTagLoop;
            var loop = 0;
            var tm = t;
            var newIndexTag = MetaMethods.NewIndex.ToMetaMethodString();
            while (loop++ < maxloop)
            {
                if (tm.IsTable)
                {
                    var table = tm.Table;
                    var oldValue = table[key];
                    var noKey = oldValue.IsDeadKey || oldValue.IsNil;
                    tm = VirtualMachine.GetMetaMethod(thread, table, newIndexTag);
                    var noMetaMethod = tm.IsNil;

                    if (noKey || noMetaMethod)
                    {
                        table[key] = value;
                        return;
                    }
                }
                else
                {
                    tm = VirtualMachine.GetMetaMethod(thread, tm, newIndexTag);
                }

                if (tm.IsFunction)
                {
                    VirtualMachine.CallMetaMethod(thread, tm, t, key, value);
                    return;
                }
                else if (tm.IsNil)
                {
                    throw new LuaRuntimeException("attempt to index a {0} value.".FormatWith(t.GetTypeString()));
                }
            }

            throw new LuaRuntimeException("loop in settable");
        }

        private static Value Arith(Thread thread, Value rkb, Value rkc, Func<double, double, double> op, MetaMethods metamethod)
        {
            if (rkb.IsNumber && rkc.IsNumber)
            {
                return Value.FromDouble(op(rkb.Number, rkc.Number));
            }

            throw new NotImplementedException();
        }

        private static Value GetMetaMethod(Thread thread, ILuaDynamicObject dynobj, string name)
        {
            return dynobj.Metatable == null
                ? Value.Nil
                : dynobj.Metatable[Value.FromString(name)];
        }

        private static Value GetMetaMethod(Thread thread, Value value, string name)
        {
            ITable metatable;
            switch (value.Type)
            {
                case ValueType.Table:
                case ValueType.UserData:
                    metatable = value.LuaObject.Metatable;
                    break;
                default:
                    metatable = thread.Environ.BasicMetatables[value.Type] as ITable;
                    break;
            }

            return metatable == null ? Value.Nil : metatable[Value.FromString(name)];
        }

        private static void CallMetaMethod(Thread thread, Value metamethod, Value p1, Value p2, Value p3)
        {
            Value result;
            VirtualMachine.CallMetaMethod(thread, metamethod, p1, p2, p3, out result);
        }

        private static void CallMetaMethod(Thread thread, Value metamethod, Value p1, Value p2, Value p3, out Value result)
        {
            throw new NotImplementedException();
        }

    }
}
